import signal
import pytest
from unittest.mock import patch, MagicMock

from motor.controller.main import main, stop_all_modules, signal_handler, modules


class TestMainModule:
    """Test main module functionality"""

    def setup_method(self):
        """Setup before each test method execution"""
        # Clear modules dict before each test
        modules.clear()

    def teardown_method(self):
        """Cleanup after each test method execution"""
        # Clear modules dict after each test
        modules.clear()

    def test_stop_all_modules_empty_dict(self):
        """Test stopping modules when dict is empty"""
        stop_all_modules()
        assert len(modules) == 0

    def test_stop_all_modules_with_stop_method(self):
        """Test stopping modules that have stop method"""
        mock_module1 = MagicMock()
        mock_module1.stop = MagicMock()
        mock_module2 = MagicMock()
        mock_module2.stop = MagicMock()
        
        modules["module1"] = mock_module1
        modules["module2"] = mock_module2
        stop_all_modules()
        
        mock_module1.stop.assert_called_once()
        mock_module2.stop.assert_called_once()

    def test_stop_all_modules_without_stop_method(self):
        """Test stopping modules that don't have stop method"""
        mock_module1 = MagicMock(spec=[])  # No stop method
        mock_module2 = MagicMock()
        mock_module2.stop = MagicMock()
        
        modules["module1"] = mock_module1
        modules["module2"] = mock_module2
        stop_all_modules()
        
        mock_module2.stop.assert_called_once()

    def test_signal_handler_sigint(self):
        """Test signal handler for SIGINT"""
        with patch('sys.exit') as mock_exit:
            signal_handler(signal.SIGINT, None)
            mock_exit.assert_called_once_with(0)

    def test_signal_handler_sigterm(self):
        """Test signal handler for SIGTERM"""
        with patch('sys.exit') as mock_exit:
            signal_handler(signal.SIGTERM, None)
            mock_exit.assert_called_once_with(0)

    def test_signal_handler_other_signal(self):
        """Test signal handler for other signals"""
        with patch('sys.exit') as mock_exit:
            signal_handler(signal.SIGUSR1, None)
            mock_exit.assert_called_once_with(0)

    @patch('motor.controller.main.logger')
    @patch('sys.exit')
    def test_signal_handler_logging(self, mock_exit, mock_logger):
        """Test signal handler logging"""
        signal_handler(signal.SIGINT, None)
        mock_logger.info.assert_called_with("All modules stopped.")

    @patch('motor.controller.main.InstanceManager')
    @patch('motor.controller.main.InstanceAssembler')
    @patch('motor.controller.main.EventPusher')
    @patch('motor.controller.main.ControllerAPI')
    @patch('motor.controller.main.controller_config')
    def test_main_without_fault_tolerance(self, mock_config, mock_api, mock_event, mock_assembler, mock_manager):
        """Test main function without fault tolerance"""
        mock_config.enable_fault_tolerance = False
        mock_config.controller_api_host = '127.0.0.1'
        mock_config.controller_api_port = 8000
        
        # Mock the modules
        mock_manager.return_value = MagicMock()
        mock_assembler.return_value = MagicMock()
        mock_event.return_value = MagicMock()
        mock_api.return_value = MagicMock()
        
        with patch('motor.controller.main.signal.signal') as mock_signal:
            with patch('builtins.input', side_effect=KeyboardInterrupt):
                with patch('sys.argv', ['main.py']):
                    main()
        
        # Verify signal handlers are registered
        assert mock_signal.call_count == 2
        mock_signal.assert_any_call(signal.SIGINT, signal_handler)
        mock_signal.assert_any_call(signal.SIGTERM, signal_handler)
        
        # Verify modules are added
        assert len(modules) == 4  # InstanceManager, InstanceAssembler, EventPusher, ControllerAPI
        assert "InstanceManager" in modules
        assert "InstanceAssembler" in modules
        assert "EventPusher" in modules
        assert "ControllerAPI" in modules

    @patch('motor.controller.main.InstanceManager')
    @patch('motor.controller.main.InstanceAssembler')
    @patch('motor.controller.main.EventPusher')
    @patch('motor.controller.main.ControllerAPI')
    @patch('motor.controller.ft.fault_manager.FaultManager')
    @patch('motor.controller.main.controller_config')
    def test_main_with_fault_tolerance(self, mock_config, mock_fault, mock_api, mock_event, mock_assembler, mock_manager):
        """Test main function with fault tolerance enabled"""
        mock_config.enable_fault_tolerance = True
        mock_config.controller_api_host = '127.0.0.1'
        mock_config.controller_api_port = 8000
        
        # Mock the modules
        mock_manager.return_value = MagicMock()
        mock_assembler.return_value = MagicMock()
        mock_event.return_value = MagicMock()
        mock_fault.return_value = MagicMock()
        mock_api.return_value = MagicMock()
        
        with patch('motor.controller.main.signal.signal') as mock_signal:
            with patch('builtins.input', side_effect=KeyboardInterrupt):
                with patch('sys.argv', ['main.py']):
                    main()
        
        # Verify fault manager is added
        assert len(modules) == 5  # All modules including FaultManager
        assert "FaultManager" in modules
        mock_fault.assert_called_once()

    @patch('motor.controller.main.InstanceManager')
    @patch('motor.controller.main.InstanceAssembler')
    @patch('motor.controller.main.EventPusher')
    @patch('motor.controller.main.ControllerAPI')
    @patch('motor.controller.main.controller_config')
    def test_main_user_input_stop(self, mock_config, mock_api, mock_event, mock_assembler, mock_manager):
        """Test main function with user input 'stop'"""
        mock_config.enable_fault_tolerance = False
        mock_config.controller_api_host = '127.0.0.1'
        mock_config.controller_api_port = 8000
        
        # Mock the modules
        mock_manager.return_value = MagicMock()
        mock_assembler.return_value = MagicMock()
        mock_event.return_value = MagicMock()
        mock_api.return_value = MagicMock()
        
        with patch('motor.controller.main.signal.signal'):
            with patch('builtins.input', return_value='stop'):
                with patch('sys.argv', ['main.py']):
                    main()
        
        # Should exit normally without exception

    @patch('motor.controller.main.InstanceManager')
    @patch('motor.controller.main.InstanceAssembler')
    @patch('motor.controller.main.EventPusher')
    @patch('motor.controller.main.ControllerAPI')
    @patch('motor.controller.main.controller_config')
    def test_main_user_input_unknown_command(self, mock_config, mock_api, mock_event, mock_assembler, mock_manager):
        """Test main function with unknown user input"""
        mock_config.enable_fault_tolerance = False
        mock_config.controller_api_host = '127.0.0.1'
        mock_config.controller_api_port = 8000
        
        # Mock the modules
        mock_manager.return_value = MagicMock()
        mock_assembler.return_value = MagicMock()
        mock_event.return_value = MagicMock()
        mock_api.return_value = MagicMock()
        
        with patch('motor.controller.main.signal.signal'):
            with patch('builtins.input', side_effect=['unknown', 'stop']):
                with patch('motor.controller.main.logger') as mock_logger:
                    with patch('sys.argv', ['main.py']):
                        main()
        
        # Should log unknown command message
        mock_logger.error.assert_called_with("Unknown command: %s", "unknown")

    @patch('motor.controller.main.InstanceManager')
    @patch('motor.controller.main.InstanceAssembler')
    @patch('motor.controller.main.EventPusher')
    @patch('motor.controller.main.ControllerAPI')
    @patch('motor.controller.main.controller_config')
    def test_main_eof_error(self, mock_config, mock_api, mock_event, mock_assembler, mock_manager):
        """Test main function with EOFError (non-interactive environment)"""
        mock_config.enable_fault_tolerance = False
        mock_config.controller_api_host = '127.0.0.1'
        mock_config.controller_api_port = 8000
        
        # Mock the modules
        mock_manager.return_value = MagicMock()
        mock_assembler.return_value = MagicMock()
        mock_event.return_value = MagicMock()
        mock_api.return_value = MagicMock()
        
        with patch('motor.controller.main.signal.signal'):
            with patch('builtins.input', side_effect=EOFError):
                with patch('time.sleep') as mock_sleep:
                    # Simulate KeyboardInterrupt after some time
                    mock_sleep.side_effect = KeyboardInterrupt()
                    with patch('sys.argv', ['main.py']):
                        main()
        
        # Should handle EOFError gracefully

    @patch('motor.controller.main.InstanceManager')
    @patch('motor.controller.main.InstanceAssembler')
    @patch('motor.controller.main.EventPusher')
    @patch('motor.controller.main.ControllerAPI')
    @patch('motor.controller.main.controller_config')
    def test_main_keyboard_interrupt(self, mock_config, mock_api, mock_event, mock_assembler, mock_manager):
        """Test main function with KeyboardInterrupt"""
        mock_config.enable_fault_tolerance = False
        mock_config.controller_api_host = '127.0.0.1'
        mock_config.controller_api_port = 8000
        
        # Mock the modules
        mock_manager.return_value = MagicMock()
        mock_assembler.return_value = MagicMock()
        mock_event.return_value = MagicMock()
        mock_api.return_value = MagicMock()
        
        with patch('motor.controller.main.signal.signal'):
            with patch('builtins.input', side_effect=KeyboardInterrupt):
                with patch('sys.argv', ['main.py']):
                    main()
        
        # Should handle KeyboardInterrupt gracefully

    @patch('motor.controller.main.logger')
    def test_main_logging(self, mock_logger):
        """Test main function logging"""
        with patch('motor.controller.main.InstanceManager'):
            with patch('motor.controller.main.InstanceAssembler'):
                with patch('motor.controller.main.EventPusher'):
                    with patch('motor.controller.main.ControllerAPI'):
                        with patch('motor.controller.main.controller_config') as mock_config:
                            mock_config.enable_fault_tolerance = False
                            mock_config.controller_api_host = '127.0.0.1'
                            mock_config.controller_api_port = 8000
                            
                            with patch('motor.controller.main.signal.signal'):
                                with patch('builtins.input', side_effect=KeyboardInterrupt):
                                    with patch('sys.argv', ['main.py']):
                                        main()
        
        # Verify logging calls
        mock_logger.info.assert_any_call("All modules started, monitoring...")
        mock_logger.info.assert_any_call("Press Ctrl+C or type 'stop' to exit.")

    @patch('motor.controller.main.logger')
    def test_main_fault_tolerance_logging(self, mock_logger):
        """Test main function fault tolerance logging"""
        with patch('motor.controller.main.InstanceManager'):
            with patch('motor.controller.main.InstanceAssembler'):
                with patch('motor.controller.main.EventPusher'):
                    with patch('motor.controller.main.ControllerAPI'):
                        with patch('motor.controller.ft.fault_manager.FaultManager'):
                            with patch('motor.controller.main.controller_config') as mock_config:
                                mock_config.enable_fault_tolerance = True
                                mock_config.controller_api_host = '127.0.0.1'
                                mock_config.controller_api_port = 8000
                                
                                with patch('motor.controller.main.signal.signal'):
                                    with patch('builtins.input', side_effect=KeyboardInterrupt):
                                        with patch('sys.argv', ['main.py']):
                                            main()
        
        # Verify fault tolerance logging - no specific message for fault tolerance in current implementation
        # The fault manager is simply added to modules when enabled

    def test_modules_global_variable(self):
        """Test that modules is a global dict"""
        assert isinstance(modules, dict)
        assert len(modules) == 0

    def test_signal_handler_with_modules(self):
        """Test signal handler with modules in the dict"""
        mock_module = MagicMock()
        mock_module.stop = MagicMock()
        modules["test_module"] = mock_module
        
        with patch('sys.exit') as mock_exit:
            signal_handler(signal.SIGINT, None)
            mock_module.stop.assert_called_once()
            mock_exit.assert_called_once_with(0)

    def test_stop_all_modules_exception_handling(self):
        """Test stop_all_modules with module that raises exception"""
        mock_module1 = MagicMock()
        mock_module1.stop = MagicMock(side_effect=Exception("Stop failed"))
        mock_module2 = MagicMock()
        mock_module2.stop = MagicMock()
        
        modules["module1"] = mock_module1
        modules["module2"] = mock_module2
        
        # Function should raise exception when first module fails
        with pytest.raises(Exception, match="Stop failed"):
            stop_all_modules()
        
        # First module should be called and fail
        mock_module1.stop.assert_called_once()
        # Second module should not be called due to exception
        mock_module2.stop.assert_not_called()

    @patch('motor.controller.main.sys.exit')
    def test_signal_handler_log_output(self, mock_exit):
        """Test signal handler log output"""
        with patch('motor.controller.main.logger') as mock_logger:
            signal_handler(signal.SIGINT, None)
            mock_logger.info.assert_any_call("Receive signal %d, exit gracefully...", 2)
            mock_exit.assert_called_once_with(0)

    def test_main_module_initialization_order(self):
        """Test that modules are initialized in correct order"""
        with patch('motor.controller.main.InstanceManager') as mock_manager:
            with patch('motor.controller.main.InstanceAssembler') as mock_assembler:
                with patch('motor.controller.main.EventPusher') as mock_event:
                    with patch('motor.controller.main.ControllerAPI') as mock_api:
                        with patch('motor.controller.main.controller_config') as mock_config:
                            mock_config.enable_fault_tolerance = False
                            mock_config.controller_api_host = '127.0.0.1'
                            mock_config.controller_api_port = 8000
                            
                            with patch('motor.controller.main.signal.signal'):
                                with patch('builtins.input', side_effect=KeyboardInterrupt):
                                    with patch('sys.argv', ['main.py']):
                                        main()
        
        # Verify initialization order
        # InstanceManager is called twice: once in main() and once in attach_observer()
        assert mock_manager.call_count == 2
        assert mock_assembler.call_count == 1
        assert mock_event.call_count == 1
        assert mock_api.call_count == 1

    def test_main_controller_api_parameters(self):
        """Test ControllerAPI initialization with correct parameters"""
        with patch('motor.controller.main.InstanceManager'):
            with patch('motor.controller.main.InstanceAssembler'):
                with patch('motor.controller.main.EventPusher'):
                    with patch('motor.controller.main.ControllerAPI') as mock_api:
                        with patch('motor.controller.main.controller_config') as mock_config:
                            mock_config.enable_fault_tolerance = False
                            mock_config.controller_api_host = '192.168.1.1'
                            mock_config.controller_api_port = 9000
                            
                            with patch('motor.controller.main.signal.signal'):
                                with patch('builtins.input', side_effect=KeyboardInterrupt):
                                    with patch('sys.argv', ['main.py']):
                                        main()
        
        # Verify ControllerAPI is called with correct parameters
        mock_api.assert_called_once_with(host='192.168.1.1', port=9000)

    def test_parse_arguments_no_config(self):
        """Test argument parsing without config parameter"""
        with patch('sys.argv', ['main.py']):
            from motor.controller.main import parse_arguments
            args = parse_arguments()
            assert args.config is None

    def test_parse_arguments_with_config(self):
        """Test argument parsing with config parameter"""
        with patch('sys.argv', ['main.py', '--config', '/path/to/config.json']):
            from motor.controller.main import parse_arguments
            args = parse_arguments()
            assert args.config == '/path/to/config.json'

    def test_parse_arguments_short_config(self):
        """Test argument parsing with short config parameter"""
        with patch('sys.argv', ['main.py', '-c', '/path/to/config.json']):
            from motor.controller.main import parse_arguments
            args = parse_arguments()
            assert args.config == '/path/to/config.json'

    @patch('motor.controller.main.set_config_path')
    @patch('motor.controller.main.reload_global_config')
    def test_main_with_config_path(self, mock_reload, mock_set_path):
        """Test main function with config path argument"""
        with patch('motor.controller.main.InstanceManager'):
            with patch('motor.controller.main.InstanceAssembler'):
                with patch('motor.controller.main.EventPusher'):
                    with patch('motor.controller.main.ControllerAPI'):
                        with patch('motor.controller.main.controller_config') as mock_config:
                            mock_config.enable_fault_tolerance = False
                            mock_config.controller_api_host = '127.0.0.1'
                            mock_config.controller_api_port = 8000
                            
                            with patch('motor.controller.main.signal.signal'):
                                with patch('builtins.input', side_effect=KeyboardInterrupt):
                                    with patch('sys.argv', ['main.py', '--config', '/test/config.json']):
                                        main()
        
        # Verify config path is set and reload is called
        mock_set_path.assert_called_once_with('/test/config.json')
        mock_reload.assert_called_once()

    @patch('motor.controller.main.set_config_path')
    @patch('motor.controller.main.reload_global_config')
    def test_main_with_config_path_reload_failure(self, mock_reload, mock_set_path):
        """Test main function with config path when reload fails"""
        mock_reload.return_value = False
        
        with patch('motor.controller.main.InstanceManager'):
            with patch('motor.controller.main.InstanceAssembler'):
                with patch('motor.controller.main.EventPusher'):
                    with patch('motor.controller.main.ControllerAPI'):
                        with patch('motor.controller.main.controller_config') as mock_config:
                            mock_config.enable_fault_tolerance = False
                            mock_config.controller_api_host = '127.0.0.1'
                            mock_config.controller_api_port = 8000
                            
                            with patch('motor.controller.main.signal.signal'):
                                with patch('builtins.input', side_effect=KeyboardInterrupt):
                                    with patch('sys.argv', ['main.py', '--config', '/test/config.json']):
                                        with patch('motor.controller.main.logger') as mock_logger:
                                            main()
        
        # Verify error logging
        mock_logger.error.assert_called_with("Failed to reload configuration with new path, using default")

    def test_main_without_config_path(self):
        """Test main function without config path argument"""
        with patch('motor.controller.main.InstanceManager'):
            with patch('motor.controller.main.InstanceAssembler'):
                with patch('motor.controller.main.EventPusher'):
                    with patch('motor.controller.main.ControllerAPI'):
                        with patch('motor.controller.main.controller_config') as mock_config:
                            mock_config.enable_fault_tolerance = False
                            mock_config.controller_api_host = '127.0.0.1'
                            mock_config.controller_api_port = 8000
                            
                            with patch('motor.controller.main.signal.signal'):
                                with patch('builtins.input', side_effect=KeyboardInterrupt):
                                    with patch('sys.argv', ['main.py']):
                                        with patch('motor.controller.main.logger') as mock_logger:
                                            main()
        
        # Verify auto-detect logging
        mock_logger.info.assert_any_call("Using auto-detected configuration file")

    @patch('motor.controller.main.InstanceManager')
    @patch('motor.controller.main.logger')
    def test_attach_observer(self, mock_logger, mock_instance_manager_class):
        """Test attach_observer function"""
        mock_instance_manager = MagicMock()
        mock_instance_manager_class.return_value = mock_instance_manager
        
        # Add some modules to test with
        modules["InstanceAssembler"] = MagicMock()
        modules["EventPusher"] = MagicMock()
        modules["FaultManager"] = MagicMock()
        modules["ControllerAPI"] = MagicMock()  # Not in observers_list
        
        from motor.controller.main import attach_observer
        attach_observer()
        
        # Verify InstanceManager is created
        mock_instance_manager_class.assert_called_once()
        
        # Verify only observers are attached
        assert mock_instance_manager.attach.call_count == 3
        mock_instance_manager.attach.assert_any_call(modules["InstanceAssembler"])
        mock_instance_manager.attach.assert_any_call(modules["EventPusher"])
        mock_instance_manager.attach.assert_any_call(modules["FaultManager"])
        
        # Verify logging
        mock_logger.info.assert_any_call("Attaching %s to instance manager", "InstanceAssembler")
        mock_logger.info.assert_any_call("Attaching %s to instance manager", "EventPusher")
        mock_logger.info.assert_any_call("Attaching %s to instance manager", "FaultManager")
        mock_logger.info.assert_any_call("All observers attached to instance manager")
